jaf.namespace("jaf.ui.form");


/** ---------------------------------- Validations ---------------------------------------------- */

/**
 * Defines some validation utility functions that can be used by validators
 */
jaf.ui.form.ValidationUtil = function() {
   return   {
      isBlank: function(objValue)   {
         if(objValue === null || objValue === undefined) {
            return true;
         }
         return (typeof(objValue) === "string" && jaf.core.Util.trim(objValue).length === 0);
      },

      isNumber: function(objValue)   {
         if(typeof(objValue) === "number")   {
            return true;
         }
         return ! isNaN(objValue);
      }
   };
}();

/**
 * Validators is an object that holds various validators that can be used by
 * forms. Each validator is a function that takes two arguments the value to
 * validate and additional optional constraints.
 * The constraints object is a special object that is used to pass additional
 * constraints to the validator. e.g. The number validator may require additional
 * checks for validation like minimum value, maximum value, positive, negative, etc
 * These checks can be specified in the constraints object. All validators can
 * also provide a special message in the constraints objects as constraints.message
 * that will be shown as the validation failure message
 */
jaf.ui.form.Validators =  {
   /**
    * This function is the mandatory field validator. Fails if the
    * the given value is null or a blank string
    */
   required: function(value) {
      return !jaf.ui.form.ValidationUtil.isBlank(value);
   },

   /**
    * Validates if the given value matches a regular expression.
    * @param {Object} value The value to validate
    * @param {Object} constraints This object should contain the pattern field
    * e.g. var constraints = {pattern: "^*.$"};
    * @return true if the value matches the pattern or false, also returns true
    * if the constraints or pattern is not specified
    */
   pattern: function(value, constraints) {
      if(constraints && constraints["pattern"])   {
         var strPattern = constraints.pattern;
         return new RegExp(strPattern).test(value);
      }
      return true;
   },

   /**
    * Validates if the specified value is a time. Additionally format validation
    * can also be done.
    * @param {Object} value The value to validate
    * @param {Object} constraints The constraints object containing a field "format"
    * <tt>
    * var constraints = {
    *    format: "hh:mm:ss"
    * };
    * </tt>
    * Three formats are currently supported:
    * <ul>
    *    <li>hh:mm:ss AMPM</li>
    *    <li>hh:mm:ss</li>
    *    <li>hh:mm (default if not specified)</li>
    * </ul>
    */
   time: function(value, constraints) {
      var arrFormats = {
         "hh:mm:ss AMPM": "^(0[1-9]|1[0-2])\:[0-5][0-9]\:[0-5][0-9]( )?(AM|am|aM|Am|PM|pm|pM|Pm)$",
         "hh:mm:ss": "^(0[0-9]|1[0-9]|2[0-3])\:[0-5][0-9]\:[0-5][0-9]$",
         "hh:mm": "^(0[0-9]|1[0-9]|2[0-3])\:[0-5][0-9]$"
      };
      var timeFormat = constraints["format"] || arrFormats["hh:mm:ss AMPM"];

      if(typeof(timeFormat) === "string") {
         var timePattern = arrFormats[timeFormat];
         if(typeof(timePattern) === "undefined")   {
            return false;
         }
         return new RegExp(timePattern).test(value);
      }
      return true;
   },

   /**
    * Validates if the given value is a date
    * @param {Object} value The value passed to validate
    * @param {Object} constraints This object contains a format for date specified by
    * constraints.format field. Currently only one format is supported: "mm-dd-yyyy"
    */
   date: function(value, constraints) {
      var arrFormats = {
         "mm-dd-yyyy" : new RegExp("^((0?[13578]|10|12)(-|\\/)((0[0-9])|([12])([0-9]?)|(3[01]?))(-|\\/)((\\d{4}))|(0?[2469]|11)(-|\\/)((0[0-9])|([12])([0-9]?)|(3[0]?))(-|\\/)((\\d{4})))$")
      }
      var dateFormat = constraints["format"] || arrFormats["mm-dd-yyyy"];

      if(typeof(dateFormat) === "string") {
         var datePattern = arrFormats[dateFormat];
         if(typeof(datePattern) === "undefined")   {
            return false;
         }
         return new RegExp(datePattern).test(value);
      }
      return true;
   },

   /**
    * Validates that given value is a number
    * @param {Object} value The value to validate
    * @param {Object} constraints This object can contain additional constraints:
    * constraints.min for minimum value check.
    * constraints.max for maximum value check.
    * <tt>
    *    var constraints = {
    *       min:30
    *       max:45
    *    }
    * </tt>
    */
   number: function(value, constraints) {
      if(isNaN(value) || typeof(value) === "undefined" || ("" + jaf.core.Util.trim(value)) === "")   {
         return false;
      }
      var num = Number(value);

      // range check
      var min = constraints["min"];
      var max = constraints["max"];

      if(min && max) {
         return num >= Number(min) && num <= Number(max);
      }else if(min)  {
         return num >= Number(min);
      }else if(max)  {
         return num <= Number(max);
      }

      // this is a number otherwise
      return true;
   },

   /**
    * Compares if the two form fields have the same value
    * e.g. This is useful for email confirmation, new password comparisons, etc.
    * @param {Object} value
    * @param {Object} constraints This object contains the ids of the fields
    * to compare for equality specified by constraints.fields, the value of
    * which must be an array of Strings
    * <tt>
    *    var constraints = {
    *       fields: ["password", "confirmPassword"]
    *    }
    * </tt>
    */
   fieldcompare: function(value, constraints) {
      var arrFieldIds = constraints["fields"];
      for(var i = 0, len = arrFieldIds.length; i < len; i++)   {
         var field = document.getElementById(arrFieldIds[i]);
         if(value !== field.value)  {
            return false;
         }
      }
      return true;
   },

   /**
    * Sepcial validator that delegates validation to a different function. This
    * is useful for custom validations for various business rules specific to
    * application
    * @param {Object} value
    * @param {Object} constraints Contains the reference to the delegate function
    * that will do the actual validation and is specified by constraints.delegate
    * e.g.
    * <tt>
    *    var constraints = {
    *       delegate: function(value, constr) {
    *          if(value === "Black Mojo" && app.blackMajicAllowed) {
    *             return true;
    *          }
    *          return false;
    *       }
    *    }
    * </tt>
    */
   delegate: function(value, constraints) {
      var delegateValidator = constraints["delegate"];
      if(typeof(delegateValidator) === "function") {
         return delegateValidator(value, constraints);
      }
      return false;
   }
};

/**
 * FieldValidator uses various validators as defined by validation information
 * to validate the fields. Renders error messages if any of the field values
 * are not valid.
 */
jaf.ui.form.FieldValidator = function(rendererFunc)  {
   var fieldValidatorMapping = {};
   var renderer = rendererFunc;

   /**
    * Validates the field specified by <tt>fieldId</tt> whose value
    * is <tt>value</tt>
    * @return Any error message as a result of validation or null
    */
   function validateField(fieldId, value) {
	      var arrValidationInfo = fieldValidatorMapping[fieldId];
	      var arrMsgs = [];
	      for(var i = 0, len = arrValidationInfo.length; i < len; i++)   {
	         var valInfo = arrValidationInfo[i];
	         var validator = jaf.ui.form.Validators[valInfo["validator"]];
	         var constraints = valInfo["constraints"] || {};
	         var message = valInfo["message"];
	         if(! validator(value, constraints))  {
	            var customMessage = constraints["message"];
	            return customMessage || message;
	         }
	      }
	      return null;
   }

   return   {
      /**
       * Adds a field validation information to this FieldValidator
       * @param {String} fieldId The id form field whose value to validate
       * @param  {Object} validatorOpts The validation options object containing
       * following information:
       * The validator to use, which must be one of the validators specified
       * in jaf.ui.form.Validators.
       * The constraints to be passed to the validator.
       * The message to show if validation fails.
       * e.g.
       * <tt>
       *    var validatorOpts = {
       *       validator: "number",
       *       constraints: {
       *          min: 0,
       *          max: 100
       *       },
       *       message: "The value must be between 0 and 100 (enclusive)"
       *    };
       * </tt>
       */
      add: function(fieldId, validatorOpts)   {
         var arrVals = fieldValidatorMapping[fieldId];
         if(typeof(arrVals) === "undefined") {
            arrVals = fieldValidatorMapping[fieldId] = [];
         }
         arrVals.push(validatorOpts);
      },

      /**
       * Renders validation messages in a special container (HTML) whose id is
       * "messages" Each message rendered is given a class name as "message error invalid"
       * These message elements are rendered as "p" elemnts inside a "div" inside
       * the message container
       */
      render: function(arrMsgs)   {
         if(renderer && typeof(renderer) == "function")  {
            renderer(arrMsgs);
         }else {
            alert(arrMsgs);
         }
         /*
         var messageContainer = document.getElementById("messages");
         if(messageContainer) {
            messageContainer.innerHTML = "";
            var div = document.createElement("div");
            div.className = "validation-messages";
            for(var i = 0, len = arrMsgs.length; i < len; i++) {
               var p = document.createElement("p");
               p.className = "message error invalid";
               p.appendChild(document.createTextNode(arrMsgs[i]));
               div.appendChild(p);
            }
            messageContainer.appendChild(div);
         }
         */
      },

      /**
       * Clears the rendered messages
       */
      clear: function(arrMsgs)   {
         var messageContainer = document.getElementById("messages");
         messageContainer.innerHTML = "";
      },

      /**
       * Validates each field added to this validator, collects the messages and
       * renders the messages if any of the validations fail.
       */
      validate: function() {
         this.clear();
         var arrMsgs = [];
         for(fieldId in fieldValidatorMapping)  {
            var field = document.getElementById(fieldId);
            var value = "";

            // check if the getValue() is supported in case of a custom component
            if(typeof(field.getValue) === "function") {
               value = field.getValue();
            }else {
               // fall back to the good ol' way
               value = field.value;
            }
            var msg = validateField(fieldId, value);
            // see if there is a message (in case of validation failure)
            if(msg)  {
               arrMsgs.push(msg);
            }
         }
         if(arrMsgs.length > 0)  {
            this.render(arrMsgs);
            return false;
         }
         return true;
      }
   };
}


/** ----------------------------------- Templating ---------------------------------------------- */

/**
 * A simple templating mechanism for creating string templates. A template has replacement tokens
 * that are of the format '{' followed by anystring followed by '}'. e.g The template below has
 * two replacement tokens: username and password
 * <p> Hello {username}. Your password is {password} </p>
 */
jaf.ui.form.Template =  function(text)  {
   /* The original template string */
   var templateStr = text;
   /* The object we store the values for tokens */
   var keyMap = {};
   /* Our regexp to search for tokens */
   var regExp = new RegExp("\\{([^\\{\\}]+)\\}", "g");
   //var regExp = new RegExp("\\{([^\\{]*)\\}[^\\}]", "g");

   /* This function internally compiles the template into a function so that we don't needt to
    * search the string for replacement for every call to process() function
    */
   var templateFunc = (function(){
      var match = regExp.exec(templateStr);

      var allParts = [], tmp = templateStr, lastIndex = 0;
      while(match !== null)   {
         // alert(match[0]);
         tmp = templateStr.substring(lastIndex, match.index);

         allParts.push(tmp);
         allParts.push(match);
         lastIndex = regExp.lastIndex;
         // alert(lastIndex);
         match = regExp.exec(templateStr);
      }
      // if there is any trailing string
      if(lastIndex < templateStr.length) {
         allParts.push(templateStr.substring(lastIndex));
      }

      // alert(allParts.join("\n--\n"));

      return function(objMap) {
         if(objMap)  {
            for(var key in objMap)  {
               keyMap[key] = objMap[key];
            }
         }

         var str = [], val = "";
         for(var i = 0, len = allParts.length; i < len; i++)  {
            var part = allParts[i];
            if(typeof(part) === "string")   {
               str.push(part);
            }else   {
               val = keyMap[part[1]] || part[0];
               str.push(val);
            }
         }
         return str.join("");
      };
   })();

   return {
      /**
       * Put a value for a given token in this template
       * @param {String} The token key as it appears in the template
       * @param {String value The value of the token
       */
      put: function(key, value) {
         keyMap[key] = value;
      },
      /**
       * Process this template. The values in the optional passed map will override those that were
       * put by the put(String, String) function of this template
       * @param {Object} objMap The object containing tokens and their values as properties
       * @return The process template with token replaced by values. The tokens for which values
       * were not provided will be present in the returned string as is.
       */
      process: function(objMap) {
         return templateFunc(objMap);
         keyMap = {};
      },

      processWithFields: function(fields) {
         var objMap = {};
         for(var i = 0, len = fields.length; i < len; i++)  {
            var f = fields[i];
            objMap[f] = Util.byId(f).value;
         }
         this.process(objMap);
      }
   };
};
